package com.songyang.yougnsql.core.data.logger;

import com.songyang.yougnsql.common.ErrorMsg;
import com.songyang.yougnsql.common.ExceptionHandler;
import com.songyang.yougnsql.common.YoungSQLException;
import com.songyang.yougnsql.core.utils.ByteUtils;

import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.Arrays;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @ClassName LoggerImpl
 * @Description 每次对底层数据操作的时候，都会记录一条日志到磁盘上，
 * 在数据库崩溃之后可以根据日志的内容恢复数据文件
 * 日志文件的格式为：[XChecksum][log1][log2]...[logn][BadTail]
 * XChecksum是一个四字节的整数，是对后续所有日志计算的校验和，
 * log1~logn是常规日志,BadTail是在数据库崩溃时，没有来得及写完的日志数据（可能不存在）
 * 每条日志的格式为：[Size][Checksum][Data]
 * Size占四个字节
 * Checksum占四个字节
 * Data所占字节长度就是Size
 * 其中size表示的是这条日志中Data的字段数，Checksum则是该条日志的校验和
 * @date 2022/12/8 15:03
 * @Author yanceysong
 * @Version 1.0
 */
public class LoggerImpl implements Logger, AutoCloseable {
    /**
     * 日志文件后缀名
     */
    public static final String LOG_SUFFIX = ".log";
    /**
     * 种子
     */
    private static final int SEED = 13331;
    /**
     * 起始文件的偏移量
     */
    private static final int OF_SIZE = 0;
    private static final int LEN_SIZE = 4;
    private static final int LEN_CHECKSUM = 4;

    private static final int LEN_X_CHECKSUM = 4;
    /**
     * 每条日志Checksum的偏移量
     */
    private static final int OF_CHECKSUM = OF_SIZE + LEN_SIZE;
    /**
     * 每条日志数据的偏移量
     */
    private static final int OF_DATA = OF_CHECKSUM + LEN_CHECKSUM;
    private final RandomAccessFile randomAccessFile;
    private final Lock lock;
    private final FileChannel fileChannel;
    private long position;  // 当前日志指针的位置
    private long fileSize;  // 初始化时记录，log操作不更新
    private int xChecksum = 0;

    LoggerImpl(RandomAccessFile raf, FileChannel fc) {
        this.randomAccessFile = raf;
        this.fileChannel = fc;
        lock = new ReentrantLock();
        init();
    }

    LoggerImpl(RandomAccessFile raf, FileChannel fc, int xChecksum) {
        this.randomAccessFile = raf;
        this.fileChannel = fc;
        this.xChecksum = xChecksum;
        lock = new ReentrantLock();

    }

    /**
     * 检查日志文件是否正确，并且移除掉未正确处理的数据（badTail）
     */
    private void checkAndRemoveBadTail() {
        //将当前的指针指向数据域
        rewind();
        try {
            int xCheck = 0;
            while (true) {
                //position会在这里变化
                byte[] log = nextLog();
                if (log == null) break;
                xCheck = getLogChecksum(xCheck, log);
            }
            //计算的如果和获取的不相等
            if (xCheck != xChecksum) {
                throw new YoungSQLException(ErrorMsg.BAD_LOG_FILE_EXCEPTION);
            }
            //截断文件，去除badTail
            truncate(position);
            randomAccessFile.seek(position);
            //恢复为原来的位置
            rewind();
        } catch (Exception e) {
            ExceptionHandler.handler(e);
        }
    }

    /**
     * 获取一条日志的校验码
     *
     * @param xCheck 初始值
     * @param log    log文件
     * @return 校验码
     */
    private int getLogChecksum(int xCheck, byte[] log) {
        for (byte b : log) {
            xCheck = xCheck * SEED + b;
        }
        return (xCheck & 0x7FFFFFFF);
    }

    /**
     * 在日志文件当中获取一条日志信息
     *
     * @return 日志信息
     */
    private byte[] nextLogWhole() {
        //如果当前的位置加上checkSum的偏移量大于整个文件大小。说明没有数据可以读了
        if (position + OF_DATA >= fileSize) {
            return null;
        }
        try {
            //开始读这条日志的大小
            ByteBuffer size_raw = ByteBuffer.allocate(LEN_SIZE);
            fileChannel.position(position);
            fileChannel.read(size_raw);
            int newLogSize = ByteUtils.parseInt(size_raw.array());
            //读取这条日志（包括size、checkSum和data）
            ByteBuffer log_raw = ByteBuffer.allocate(OF_CHECKSUM + newLogSize);
            fileChannel.position(position);
            fileChannel.read(log_raw);
            //开始校验数据是否正确
            byte[] log = log_raw.array();
            //这条日志真正地校验和
            int checkSum1 = getLogChecksum(0, Arrays.copyOfRange(log, OF_DATA, log.length));
            //获取这条日志的校验和
            int checkSum2 = ByteUtils.parseInt(Arrays.copyOfRange(log, OF_CHECKSUM, OF_DATA));
            //比对校验和
            if (checkSum1 != checkSum2) {
                return null;
            }
            position += log.length;
            return log;
        } catch (IOException e) {
            ExceptionHandler.handler(e);
        }
        return null;
    }

    /**
     * 初始化Logger
     * 该操作会校验log文件、删除batTail（如果有）
     */
    private void init() {
        try {
            long realSize = randomAccessFile.length();
            if (realSize < 4) throw new YoungSQLException(ErrorMsg.BAD_LOG_FILE_EXCEPTION);
            //读取XCheckSum的值
            ByteBuffer raw = ByteBuffer.allocate(OF_CHECKSUM);
            fileChannel.position(0);
            fileChannel.read(raw);
            this.xChecksum = ByteUtils.parseInt(raw.array());
            this.fileSize = realSize;
            //检查并且删除掉badTail
            checkAndRemoveBadTail();
        } catch (IOException | YoungSQLException e) {
            ExceptionHandler.handler(e);
        }
    }

    /**
     * 更新XCheckSum的校验和
     *
     * @param log 打包好的日志数据
     */
    private void updateXChecksum(byte[] log) {
        this.xChecksum = getLogChecksum(this.xChecksum, log);
        try {
            fileChannel.position(0);
            fileChannel.write(ByteBuffer.wrap(ByteUtils.int2Byte(xChecksum)));
            fileChannel.force(false);
        } catch (IOException e) {
            ExceptionHandler.handler(e);
        }
    }

    /**
     * 将日志的数据打包成log的格式
     *
     * @param data 数据
     * @return 正确的格式
     */
    private byte[] wrapLog(byte[] data) {
        //计算checkSum
        byte[] checksum = ByteUtils.int2Byte(getLogChecksum(0, data));
        //计算大小
        byte[] size = ByteUtils.int2Byte(data.length);
        //拼起来
        return ByteUtils.concat(size, checksum, data);
    }

    /**
     * 插入一条日志数据
     *
     * @param data 要插入的日志数据
     */
    @Override
    public void insertLog(byte[] data) {
        //传过来的是日志的数据，需要打包成一个log的格式
        byte[] log = wrapLog(data);
        ByteBuffer buf = ByteBuffer.wrap(log);
        lock.lock();
        try {
            //移动到末尾
            fileChannel.position(fileChannel.size());
            fileChannel.write(buf);
            //出于性能方面的考虑，操作系统可能会先将 FileChannel 中的数据缓存在内存中，而不是直接写到磁盘。
            fileChannel.force(false);
        } catch (IOException e) {
            ExceptionHandler.handler(e);
        } finally {
            lock.unlock();
        }
        updateXChecksum(log);
    }

    /**
     * 将文件截断为给定的大小
     *
     * @param size 给定的大小
     * @throws Exception 异常
     */
    @Override
    public void truncate(long size) throws Exception {
        lock.lock();
        try {
            fileChannel.truncate(size);
        } finally {
            lock.unlock();
        }
    }

    /**
     * 返回下一条log的数据,如果为null则代表结束
     *
     * @return 下一条log的数据
     */
    @Override
    public byte[] nextLog() {
        lock.lock();
        try {
            //获取下一条的数据
            byte[] log = nextLogWhole();
            //返回数据域
            return log == null ? null : Arrays.copyOfRange(log, OF_DATA, log.length);
        } finally {
            lock.unlock();
        }
    }

    @Override
    public void rewind() {
        position = LEN_X_CHECKSUM;
    }

    @Override
    public void close() {
        try {
            fileChannel.close();
            randomAccessFile.close();
        } catch (IOException e) {
            ExceptionHandler.handler(e);
        }
    }
}
